package com.xbj.voa.common.shiroAndJwt;

import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpStatus;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Scope;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

/**
 * @author: 木火
 * @create: 2024-01-07
 * @description:shiro过滤器.拦截请求判断是否被shiro处理
 **/
@Component
@Scope("prototype")  //必须设为多例,不然往ThreadLocalToken中保存token,会有问题
public class ShiroFilter extends AuthenticatingFilter {
    private static Logger LOG = LoggerFactory.getLogger(ShiroFilter.class);

    @Resource
    private JwtUtil jwtUtil;

    @Value("${voa.jwt.cache-expire}")
    private int cacheExpire;

    @Resource
    private ThreadLocalToken threadLocalToken;

    @Resource
    private RedisTemplate   redisTemplate;

    /**
     * @Author 木火
     * @Date  2024/1/8
     * @Param
     * @Description 拦截请求后将令牌字符串封装成令牌对象
     **/
    @Override
    protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        LOG.debug("ShiroFilter-createToken");

        String token = getRequestToken((HttpServletRequest) servletRequest);
        if(StringUtils.isBlank(token)){
            return null ;
        }
        return new ShiroToken(token);
    }

    /**
     * @Author 木火
     * @Date  2024/1/8
     * @Param
     * @Description 拦截请求,判断是否被shiro处理 true被放行,false被拦截处理  只放行options请求
     **/
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        LOG.debug("ShiroFilter-isAccessAllowed");

        HttpServletRequest req = (HttpServletRequest) request;
        //Ajax提交的application/json数据时,会先发出options请求,试探询问服务器是否支持,在发post请求
        if(StringUtils.equals(RequestMethod.OPTIONS.name(),req.getMethod())){
            return true;
        }
        return false;
    }

    /**
     * @Author 木火
     * @Date  2024/1/8
     * @Param
     * @Description 处理被shiro拦截的请求.进行token校验
     **/
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        LOG.debug("ShiroFilter-onAccessDenied");

        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        response.setContentType("text/html");
        response.setCharacterEncoding("UTF-8");
        //允许跨域请求
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));

        threadLocalToken.clear();
        //获取请求中的token,若不存在,直接返回401
        String token = getRequestToken(request);
        if(StringUtils.isBlank(token)){
            LOG.info("请求中没有token");
            //401:未授权,请求要求进行身份验证
            response.setStatus(HttpStatus.SC_UNAUTHORIZED);
            response.getWriter().print("无效的令牌");
            return false;
        }
        //校验令牌是否过期
        try {
            jwtUtil.verifierToken(token);
        } catch (TokenExpiredException e) {
            //若客户端令牌过期,则校验redis中是否过期,若没有过期则新生成一个,传给客户端
            if(redisTemplate.hasKey(token)){
                redisTemplate.delete(token);
                int userId = jwtUtil.getUserId(token);
                token = jwtUtil.createToken(userId);
                redisTemplate.opsForValue().set(token,userId+"",cacheExpire, TimeUnit.DAYS);
                threadLocalToken.setToken(token);
            }else {
                //服务端令牌过期,则重新登陆
                //401:未授权,请求要求进行身份验证
                response.setStatus(HttpStatus.SC_UNAUTHORIZED);
                response.getWriter().print("登陆过期,请重新登陆");
            }
        }catch (JWTDecodeException e){
            //401:未授权,请求要求进行身份验证
            response.setStatus(HttpStatus.SC_UNAUTHORIZED);
            response.getWriter().print("无效的令牌");
            return false;
        }
        //调用ShiroRealm类,进行认证与授权
        boolean bool = executeLogin(request, response);

        return bool;
    }

    /**
     * @Author 木火
     * @Date  2024/1/8
     * @Param
     * @Description ShiroRealm.doGetAuthenticationInfo 认证失败时调用
     **/
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        LOG.debug("ShiroFilter-onLoginFailure");

        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;
        //401:未授权,请求要求进行身份验证
        res.setStatus(HttpStatus.SC_UNAUTHORIZED);
        res.setContentType("application/json;charset=utf-8");
        res.setHeader("Access-Control-Allow-Credentials","true");
        res.setHeader("Access-Control-Allow-Origin",req.getHeader("Origin"));
        try {
            res.getWriter().print(e.getMessage());
        } catch (IOException ex) {
            LOG.debug("ShiroFilter-onLoginFailure,IOException:"+e.getMessage());
        }
        return false;
    }

    /**
     * @Author 木火
     * @Date  2024/1/8
     * @Param
     * @Description 掌管拦截请求与响应
     **/
    @Override
    public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
        super.doFilterInternal(request, response, chain);
    }

    /**
     * @Author 木火
     * @Date  2024/1/8
     * @Param
     * @Description 获取请求头或请求参数中的token
     **/
    private String getRequestToken(HttpServletRequest request){
        String token = request.getHeader("token");
        if(StringUtils.isBlank(token)){
            token = request.getParameter("token");
        }
        return token;
    }
}
